﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.ComponentModel;

namespace JasLib.Interop.Win32
{
    public static partial class User32Helper
    {
        #region GetWindowLongPtr

        /// <summary>
        /// This will only get called on 32-bit, otherwise the parameters will be wrong.
        /// </summary>
        [DllImport(Constants.ModuleNames.USER32, EntryPoint = "GetWindowLong", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
        private static extern INT_PTR GetWindowLong_EntryPoint(HWND hWnd, WindowLongPtrOffset nIndex);

        /// <summary>
        /// This does not exist on 32-bit.
        /// </summary>
        [DllImport(Constants.ModuleNames.USER32, EntryPoint = "GetWindowLongPtr", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
        private static extern INT_PTR GetWindowLongPtr_EntryPoint(HWND hWnd, WindowLongPtrOffset nIndex);

        [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Unicode)]
        private delegate INT_PTR GetWindowLongPtr_Delegate(HWND hWnd, WindowLongPtrOffset nIndex);

        private static GetWindowLongPtr_Delegate s_GetWindowLongPtr_Delegate = INT_PTR.Size == sizeof(Int32) ? new GetWindowLongPtr_Delegate(GetWindowLong_EntryPoint) : new GetWindowLongPtr_Delegate(GetWindowLongPtr_EntryPoint);

        /// <summary>
        /// This alleviates the confusion between GetWindowLong and GetWindowLongPtr. It can be called safely and uniformly from any platform.
        /// </summary>
        public static INT_PTR GetWindowLongPtr(HWND hWnd, WindowLongPtrOffset nIndex)
        {
            return s_GetWindowLongPtr_Delegate(hWnd, nIndex);
        }

        #endregion

        #region SetWindowLongPtr

        /// <summary>
        /// This will only get called on 32-bit, otherwise the parameters will be wrong.
        /// </summary>
        [DllImport(Constants.ModuleNames.USER32, EntryPoint = "SetWindowLong", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
        private static extern INT_PTR SetWindowLong_EntryPoint(HWND hWnd, WindowLongPtrOffset nIndex, INT_PTR dwNewLong);

        /// <summary>
        /// This does not exist on 32-bit.
        /// </summary>
        [DllImport(Constants.ModuleNames.USER32, EntryPoint = "SetWindowLongPtr", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
        private static extern INT_PTR SetWindowLongPtr_EntryPoint(HWND hWnd, WindowLongPtrOffset nIndex, INT_PTR dwNewLong);

        [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Unicode)]
        private delegate INT_PTR SetWindowLongPtr_Delegate(HWND hWnd, WindowLongPtrOffset nIndex, INT_PTR dwNewLong);

        private static SetWindowLongPtr_Delegate s_SetWindowLongPtr_Delegate = INT_PTR.Size == sizeof(Int32) ? new SetWindowLongPtr_Delegate(SetWindowLong_EntryPoint) : new SetWindowLongPtr_Delegate(SetWindowLongPtr_EntryPoint);

        /// <summary>
        /// This alleviates the confusion between SetWindowLong and SetWindowLongPtr. It can be called safely and uniformly from any platform.
        /// </summary>
        public static INT_PTR SetWindowLongPtr(HWND hWnd, WindowLongPtrOffset nIndex, INT_PTR dwNewLong)
        {
            return s_SetWindowLongPtr_Delegate(hWnd, nIndex, dwNewLong);
        }

        #endregion

        /// <summary>
        /// Override that finds the deepest descendant at the given client coordinates.
        /// This matches the general expectation that most people have with the Win32 ChildWindowFromPoint API.
        /// WindowFromPoint only starts at the global level and ChildWindowFromPoint/RealChildWindowFromPoint/ChildWindowFromPointEx
        /// </summary>
        public static HWND DeepestChildWindowFromClientPoint(HWND hParent, POINT ptParentClientCoords)
        {
            POINT ptStartingParentScreen = ptParentClientCoords;
            if (!USER32.ClientToScreen(hParent, ref ptStartingParentScreen))
                return HWND.NULL;

            HWND hCurrentParent = hParent;
            HWND hCurrentChild = hCurrentParent;
            while (hCurrentChild.IsWindow)
            {
                POINT ptCurrentParentClient = ptStartingParentScreen;
                if (!USER32.ScreenToClient(hCurrentParent, ref ptCurrentParentClient))
                    return HWND.NULL;

                hCurrentChild = USER32.ChildWindowFromPoint(hCurrentParent, ptCurrentParentClient);
                if (hCurrentChild == hCurrentParent)
                    break;

                hCurrentParent = hCurrentChild;
            }

            return hCurrentParent;
        }

        #region SetWindowsHookEx

        /// <summary>
        /// This preserves hook callbacks from getting garbage-collected.
        /// </summary>
        private static Dictionary<HHOOK, Delegate> s_hookDelegateReferences = new Dictionary<HHOOK, Delegate>();

        public static HHOOK SetWindowsHookEx(CBTProc pfnhook, HINSTANCE hinst = default(HINSTANCE), uint threadid = 0)
        {
            HHOOK hHook = USER32.SetWindowsHookEx(HookId.WH_CBT, pfnhook, hinst, threadid);
            if (hHook.IsInvalid)
                throw new Win32Exception();
            lock (s_hookDelegateReferences)
                s_hookDelegateReferences.Add(hHook, pfnhook);
            return hHook;
        }

        public static HHOOK SetWindowsHookEx(CallWndProc pfnhook, HINSTANCE hinst = default(HINSTANCE), uint threadid = 0)
        {
            HHOOK hHook = USER32.SetWindowsHookEx(HookId.WH_CALLWNDPROC, pfnhook, hinst, threadid);
            if (hHook.IsInvalid)
                throw new Win32Exception();
            lock (s_hookDelegateReferences)
                s_hookDelegateReferences.Add(hHook, pfnhook);
            return hHook;
        }

        public static HHOOK SetWindowsHookEx(CallWndRetProc pfnhook, HINSTANCE hinst = default(HINSTANCE), uint threadid = 0)
        {
            HHOOK hHook = USER32.SetWindowsHookEx(HookId.WH_CALLWNDPROCRET, pfnhook, hinst, threadid);
            if (hHook.IsInvalid)
                throw new Win32Exception();
            lock (s_hookDelegateReferences)
                s_hookDelegateReferences.Add(hHook, pfnhook);
            return hHook;
        }

        public static HHOOK SetWindowsHookEx(GetMsgProc pfnhook, uint threadid = 0)
        {
            HHOOK hHook = USER32.SetWindowsHookEx(HookId.WH_CALLWNDPROC, pfnhook, default(HINSTANCE), threadid);
            if (hHook.IsInvalid)
                throw new Win32Exception();
            lock (s_hookDelegateReferences)
                s_hookDelegateReferences.Add(hHook, pfnhook);
            return hHook;
        }

        /// <remarks>
        /// If the user presses CTRL+ESC OR CTRL+ALT+DEL during journal playback, the system stops the playback, unhooks the journal playback procedure, and posts a WM_CANCELJOURNAL message to the journaling application.
        /// </remarks>
        public static HHOOK SetWindowsHookEx(JournalPlaybackProc pfnhook, HINSTANCE hinst = default(HINSTANCE))
        {
            return USER32.SetWindowsHookEx(HookId.WH_JOURNALPLAYBACK, pfnhook, hinst, 0);
        }

        public static HHOOK SetWindowsHookEx(KeyboardProc pfnhook, HINSTANCE hinst = default(HINSTANCE), uint threadid = 0)
        {
            return USER32.SetWindowsHookEx(HookId.WH_KEYBOARD, pfnhook, hinst, threadid);
        }

        public static void UnhookWindowsHookEx(HHOOK hhook)
        {
            hhook.Dispose();
            lock (s_hookDelegateReferences)
                s_hookDelegateReferences.Remove(hhook);
            return;
        }

        #endregion
    }
}
